[アップデート] Amazon CloudWatch Synthetics で Playwright が使えるようになりました
いわさです。
Amazon CloudWatch Synthetics を使うスクリプトで定義したブラウザ操作をスケジュール実行し Web ページや API などのエンドポイントを監視することが出来ます。
このスクリプトですが、これまで Puppeteer (Node.js) あるいは Selenium Webdriver (Python) を使ってヘッドレス Chrome を実行することが出来ていました。
先日のアップデートで、新たに Playwright がサポートされるようになりました。
Playwright は Microsoft が開発するフレームワークで DevIO でも何度か紹介されています。E2E テストで導入されている方も多いのではないでしょうか。
Synthetics 上で Playwright が使えるランタイムは本日時点では Node.js のみですが、既存の Playwright スクリプトを流用したり、あるいはネイティブ Playwright の高度な機能を使うことが出来るようになります。
また、アナウンスでも少し触れられていますが Playwright ではマルチタブを扱うことも出来ます。
また、従来のカナリアスクリプトでは独自のログファイルを出力する方式だったのですが、Playwright の場合は CloudWatch Logs 上へログが出力されるようになっています。これによって CloudWatch Logs Insights などの機能を活用してログ分析を行うことも出来るようになっています。
ブループリントから選択
Synthetics Canaries のスクリプトエディタを確認してみるとランタイムバージョンとしてsyn-nodejs-playwright-1.0
が追加されていることが確認出来ます。
Node.js のバージョンは 20.x、Playwright のバージョンは 1.44.1 です。
注意点として、いくつかのブループリントでは Playwright がまだサポートされていないものがあります。
確認した限りではハートビートと GUI ワークフロービルダーのみが使用可能でした。
まぁハートビートあたりで初期構築出来ればどうにでも頑張れそうな気もしますが。
他の Puppeteer や Selenium と違う点として設定ファイルが別で管理されています。
例えばマネジメントコンソール上の「スクリーンショットを撮る」オプションなどを変更すると、設定ファイルの以下の値が変更されます。
一方で監視先 URL はハードコーディングされていたりと、あくまでも Sysntetics 用の Playwright ライブラリで使われる共通設定のみが設定ファイルに切り出されている感じです。
実際に適当な Web サイトを対象に実行してみました。
ログに関しては次のように CloudWatch Logs へ出力される形になっていました。
一方で他のフレームワークについては従来のどおりの独自テキストファイルでの出力でした。
Lambda レイヤーを眺めてみる
Synthetics Canary は裏で Lambda 関数が作成されます。
@amzn/synthetics-playwright
にラップされているので中身の実装がわかりにくいのですが、気になる人は次のレイヤーを調べてみてください。
get-layer-version-by-arn
で Location を調べて Unzip しました。
% aws lambda get-layer-version-by-arn --arn arn:aws:lambda:ap-northeast-1:172291836251:layer:AWS-CW-SyntheticsNodeJsPlaywright:1
{
"Content": {
"Location": "https://awslambda-ap-ne-1-layers.s3.ap-northeast-1.amazonaws.com/snapshots/172291836251/AWS-CW-SyntheticsNodeJsPlaywright-78779771-c8e6-4912-9966-d50f7074a756?versionId= ... &X-Amz-Signature=5a19fff9c4f519e8cf0627378192496b0aad83baa1160180bd0b8236ebc0e169",
"CodeSha256": "0Fhg8YdG3Ir4C1gkXPPTebZoSCGNqZAaAZLcF2ZToz0=",
"CodeSize": 115544034
},
"LayerArn": "arn:aws:lambda:ap-northeast-1:172291836251:layer:AWS-CW-SyntheticsNodeJsPlaywright",
"LayerVersionArn": "arn:aws:lambda:ap-northeast-1:172291836251:layer:AWS-CW-SyntheticsNodeJsPlaywright:1",
"Description": "Synthetics library for syn-nodejs-playwright-1.0 with NodeJS 20.x and Playwright 1.44.1",
"CreatedDate": "2024-11-20T01:36:15.485+0000",
"Version": 1,
"CompatibleRuntimes": [
"nodejs20.x"
]
}
% wget "https://awslambda-ap-ne-1-layers.s3.ap-northeast-1.amazonaws.com/snapshots/172291836251/AWS-CW-SyntheticsNodeJsPlaywright-78779771-c8e6-4912-9966-d50f7074a756?versionId= ... &X-Amz-Signature=5a19fff9c4f519e8cf0627378192496b0aad83baa1160180bd0b8236ebc0e169"
:
Resolving awslambda-ap-ne-1-layers.s3.ap-northeast-1.amazonaws.com (awslambda-ap-ne-1-layers.s3.ap-northeast-1.amazonaws.com)... 52.219.162.106, 3.5.156.105, 52.219.8.55, ...
Connecting to awslambda-ap-ne-1-layers.s3.ap-northeast-1.amazonaws.com (awslambda-ap-ne-1-layers.s3.ap-northeast-1.amazonaws.com)|52.219.162.106|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 115544034 (110M) [application/zip]
Saving to: ‘AWS-CW-SyntheticsNodeJsPlaywright-78779771-c8e6-4912-9966-d50f7074a756?versionId=WyDymK8tmbOWPhaJ5NNDfc2EOBWEJapc&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEKL%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLW5vcnRoZWFzdC0xIkYwRAIgH%2FIP4y69B%2FCZw+1R9’
AWS-CW-SyntheticsNodeJsPlaywright-7877 100%[===========================================================================>] 110.19M 7.35MB/s in 15s
2024-11-28 04:13:41 (7.50 MB/s) - ‘AWS-CW-SyntheticsNodeJsPlaywright-78779771-c8e6-4912-9966-d50f7074a756?versionId=WyDymK8tmbOWPhaJ5NNDfc2EOBWEJapc&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEKL%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLW5vcnRoZWFzdC0xIkYwRAIgH%2FIP4y69B%2FCZw+1R9’ saved [115544034/115544034]
% unzip 'AWS-CW-SyntheticsNodeJsPlaywright-78779771-c8e6-4912-9966-d50f7074a756?versionId=WyDymK8tmbOWPhaJ5NNDfc2EOBWEJapc&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEKL%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLW5vcnRoZWFzdC0xIkYwRAIgH%2FIP4y69B%2FCZw+1R9'
:
extracting: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/types/TreeRuleObject.js
inflating: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/types/EndpointError.js
extracting: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/types/RuleSetObject.js
extracting: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/types/ErrorRuleObject.js
creating: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/lib/
extracting: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/lib/isIpAddress.js
creating: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/lib/aws/
inflating: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/lib/aws/partitions.json
inflating: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/lib/aws/parseArn.js
inflating: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/lib/aws/index.js
inflating: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/lib/aws/partition.js
inflating: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/lib/aws/isVirtualHostableS3Bucket.js
creating: nodejs/node_modules/@playwright/
creating: nodejs/node_modules/@playwright/test/
inflating: nodejs/node_modules/@playwright/test/package.json
inflating: nodejs/node_modules/@playwright/test/index.js
inflating: nodejs/node_modules/@playwright/test/README.md
inflating: nodejs/node_modules/@playwright/test/index.mjs
inflating: nodejs/node_modules/@playwright/test/index.d.ts
inflating: nodejs/node_modules/@playwright/test/cli.js
inflating: nodejs/node_modules/@playwright/test/reporter.d.ts
inflating: nodejs/node_modules/@playwright/test/NOTICE
inflating: nodejs/node_modules/@playwright/test/LICENSE
inflating: nodejs/node_modules/@playwright/test/reporter.js
inflating: nodejs/node_modules/@playwright/test/reporter.mjs
synthetics-playwright のpackage.json
は次のような感じでした。
追っていってみるとsynthetics-core
とsynthetics-agent-client
が使われていますね。
{
"name": "@amzn/synthetics-playwright",
"version": "0.1.0",
"license": "UNLICENSED",
"repository": "ssh://git.amazon.com/pkg/SyntheticsPlaywright-NodeJsSDK",
"homepage": "https://code.amazon.com/packages/SyntheticsPlaywright-NodeJsSDK",
"engines": {
"node": ">=18"
},
"scripts": {
"clean": "rm -rf ./build && rm -rf ./dist && rm -rf ./node_modules && rm -rf package-lock.json || true",
"build": "npm run lcheck && tsc",
"watch": "tsc -w",
"prepublishOnly": "npm run build",
"package-artifacts": "build-tools/bin/npm-package-artifacts",
"zip-artifacts": "build-tools/bin/npm-zip-artifacts",
"post-npm-pretty-much": "npm run package-artifacts && npm run zip-artifacts",
"release": "npm run build",
"lcheck": "eslint './src/**/*.ts' --max-warnings 0",
"lfix": "npm run -s lcheck -- --fix",
"test": "jest --collectCoverage --collectCoverageFrom=src/__tests__/*.{ts,js}",
"posttest": "generate-coverage-data --language typescript",
"dev-canary": "npm run release && npm run package-artifacts && brazil-build-tool-exec sam build && brazil-build-tool-exec sam local invoke LocalDevCanary -e local-dev/events/event.json"
},
"main": "./dist/index.js",
"exports": "./dist/index.js",
"files": [
"dist/"
],
"npm-pretty-much": {
"publishLibCommonJs": true
},
"dependencies": {
"playwright": "1.44.1",
"@playwright/test": "1.44.1",
"@amzn/synthetics-core": "^0.1.0"
},
"devDependencies": {
"@amzn/brazil": "^2.0.6",
"@tsconfig/node18": "^18.2.4",
"@types/jest": "^29.5.12",
"@types/aws-lambda": "^8.10.109",
"@types/aws-lambda-mock-context": "^3.2.0",
"@babel/core": "^7.16.0",
"@babel/preset-env": "^7.16.4",
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"typescript": "^5.4.5",
"@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "^6.18.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-prettier": "^5.1.3"
}
}
デフォルトで使われているメインのステップ処理部分はこんな感じになっていました。
:
async executeStep(stepName, func, stepConfig, pageObj) {
// Add validation for stepName
const stepNamePattern = /^[-a-zA-Z0-9._/]+$/;
if (!stepNamePattern.test(stepName)) {
throw new Error(`Invalid step name: "${stepName}". The step name can only contain letters, numbers, hyphens, underscores and periods.`);
}
this.logger.setStepName(stepName);
if (!this.config) {
this.config = await super.getConfiguration();
}
// Merge the passed stepConfig with the existing configuration here
const mergedStepConfig = {
...this.config.stepConfiguration,
...(stepConfig || {}),
};
const currentPage = pageObj || this.page;
const sourceURL = stepUtils.getUrl(currentPage, stepName);
let sourceScreenshot = undefined;
if (mergedStepConfig?.screenshotOnStepStart) {
sourceScreenshot = await stepUtils.takeScreenShot(currentPage, stepName, constants_1.START_SUFFIX);
}
let stepDetails = undefined;
stepDetails = await this.startStep(stepName, sourceURL, sourceScreenshot, stepConfig);
try {
const returnValue = await func();
const destinationURL = stepUtils.getUrl(currentPage, stepName);
await this.succeedStep(stepName, destinationURL);
if (mergedStepConfig?.screenshotOnStepSuccess) {
await stepUtils.takeScreenShot(currentPage, stepName, constants_1.SUCCEED_SUFFIX);
}
return returnValue;
}
catch (error) {
const destinationURL = stepUtils.getUrl(currentPage, stepName);
await this.failStep(stepName, stepUtils.formatError(error), destinationURL);
if (mergedStepConfig?.screenshotOnStepFailure) {
await stepUtils.takeScreenShot(currentPage, stepName, constants_1.FAILED_SUFFIX);
}
if (!mergedStepConfig?.continueOnStepFailure ||
stepUtils.formatError(error).includes(constants_1.SUCCEED_STEP_ERROR)) {
const stackTrace = (0, util_1.inspect)(error);
const errorMessage = `Step ${stepDetails.stepNum}: ${stepName} failed with: ${stackTrace}`;
throw new Error(errorMessage);
}
}
finally {
await this.endStep(stepName);
this.logger.resetStepName();
}
}
:
あまり中身を意識する必要もないのですが、既存 Playwright スクリプトを Synthetics に流用しようとする際に AWS 側のレイヤーについて把握したい場合があると思うので、その際に見てみると良いのではないでしょうか。
例えばアナウンスにあったマルチタブの機能は、特に Synthetics 側で何か用意されているわけではなさそうなので、Playwright 側の Page オブジェクトを普通に複数使う感じで良さそうでした。
さいごに
本日は Amazon CloudWatch Synthetics で Playwright が使えるようになったので使ってみました。
ブループリントからほぼ変えずにハートビート定期実行するくらいであればたいした恩恵はない気もしますが、ゴリゴリにスクリプトをカスタマイズするような方は Playwright に乗り換えると楽になるのではないでしょうか。ぜひ使ってみてください。